Análise de performance das queries ao MongoDB¶

Nesta análise vamos computar o tempo de resposta de diferentes queries que serão utilizadas na API do AQE.

Existem quatro tipos de queries principais que serão avaliadas, com algumas variações:

  1. Consulta termos de uma determinada fonte

    • Incluindo outras fontes
      collection.find({"source": "TULSA_THESAURUS"})
      
    • Exclusivo determinada fonte.
      collection.find({"source": ["TULSA_THESAURUS"]})
      
  2. Consulta termos relacionados a um determinado texto

    • Match Absoluto
      collection.find({"text": "coral"})
      
    • Match Regex
      collection.find({"text": { "$regex": ".*coral.*" } })
      
  3. Consulta termos relacionados a um determinado texto e com um tipo de relacionamento

    • Permite outras relações
      collection.find({"text": "coral", "terms.termRelation": "RT"})
      
    • Match de pelo menos um na lista
      collection.find({"text": "coral", "terms.termRelation": { "$in": ["RT", "NT"]}})
      
    • Match de todos na lista
      collection.find({"text": { "$regex": ".*coral.*"}, "terms.termRelation": { "$all": ["RT", "NT"]}})
      
  4. Consulta termos relacionados a um determinado texto com uma quantidade de passos no grafo

    • Consulta com uma quantidade p de passos
      collection.aggregate([
              {
                  '$match': {
                      'text': {
                          '$in': [
                              'coral'
                          ]
                      }, 
                      'terms': {
                          '$ne': []
                      }
                  }
              }, {
                  '$graphLookup': {
                      'from': 'termsBase', 
                      'startWith': '$terms.text', 
                      'connectFromField': 'terms.text', 
                      'connectToField': 'text', 
                      'as': 'relatedTerms', 
                      'depthField': 'depth', 
                      'maxDepth': p
                  }
              }
          ])
      

Inicializando a conexão com o MongoDB¶

In [1]:
import os
import time
from pathlib import Path

from pymongo import MongoClient, TEXT
import pandas as pd
import plotly.express as px

BASE_DIR = Path().resolve().parent
os.chdir(BASE_DIR)

from conf.config import settings
In [2]:
client = MongoClient(settings.MONGODB_URI)
collection = client[settings.MONGODB_DATABASE_NAME][settings.MONGODB_COLLECTION_NAME]
depth_step = 0 

Para rodar todas as consultas da melhor maneira possível, é necessário criar os índices antes de inserir os dados. Para isso, no compass rode o seguinte comando:

db.termsBase.createIndex({text: "text"}, { language_override: "none"}, unique: true)

Computando tempo das queries¶

Vamos agora definir as queries e seus atributos e computar o tempo de resposta rodando 50 vezes cada query.

In [3]:
queries = [
    {
        "query": {"source": "TULSA_THESAURUS"},
        "query_type": "find",
        "classe_query": "Busca por source",
        "detalhe_query": "Permite outras fontes"
    },
    {
        "query": {"source": ["TULSA_THESAURUS"]},
        "query_type": "find",
        "classe_query": "Busca por source",
        "detalhe_query": "Exclusivo da fonte especificada"
    },
    {
        "query": {"text": "coral"},
        "query_type": "find",
        "classe_query": "Busca por termo",
        "detalhe_query": "Match absoluto"
    },
    {
        "query": {"text": { "$regex": ".*coral.*"}},
        "query_type": "find",
        "classe_query": "Busca por termo",
        "detalhe_query": "Match com regex"
    },
    {
        "query": {"text": "coral", "terms.termRelation": "RT"},
        "query_type": "find",
        "classe_query": "Busca por termo e relação",
        "detalhe_query": "Permite outras relações"
    },
    {
        "query": {"text": "coral", "terms.termRelation": { "$in": ["RT", "NT"]}},
        "query_type": "find",
        "classe_query": "Busca por termo e relação",
        "detalhe_query": "Match de pelo menos um na lista"
    },
    {
        "query": {"text": { "$regex": ".*coral.*"}, "terms.termRelation": { "$all": ["RT", "NT"]}},
        "query_type": "find",
        "classe_query": "Busca por termo e relação",
        "detalhe_query": "Match de todos na lista"
    },
    {
        "query": [
            {
                '$match': {
                    'text': {
                        '$in': [
                            'coral'
                        ]
                    }, 
                    'terms': {
                        '$ne': []
                    }
                }
            }, {
                '$graphLookup': {
                    'from': 'termsBase', 
                    'startWith': '$terms.text', 
                    'connectFromField': 'terms.text', 
                    'connectToField': 'text', 
                    'as': 'relatedTerms', 
                    'depthField': 'depth', 
                    'maxDepth': 0
                }
            }
        ],
        "query_type": "aggregate",
        "classe_query": "Busca por termos caminhando no grafo",
        "detalhe_query": "Caminha 2 arestas do grafo"
    },
    {
        "query": [
            {
                '$match': {
                    'text': {
                        '$in': [
                            'coral'
                        ]
                    }, 
                    'terms': {
                        '$ne': []
                    }
                }
            }, {
                '$graphLookup': {
                    'from': 'termsBase', 
                    'startWith': '$terms.text', 
                    'connectFromField': 'terms.text', 
                    'connectToField': 'text', 
                    'as': 'relatedTerms', 
                    'depthField': 'depth', 
                    'maxDepth': 1
                }
            }
        ],
        "query_type": "aggregate",
        "classe_query": "Busca por termos caminhando no grafo",
        "detalhe_query": "Caminha 3 arestas do grafo"
    },
    {
        "query": [
            {
                '$match': {
                    'text': {
                        '$in': [
                            'coral'
                        ]
                    }, 
                    'terms': {
                        '$ne': []
                    }
                }
            }, {
                '$graphLookup': {
                    'from': 'termsBase', 
                    'startWith': '$terms.text', 
                    'connectFromField': 'terms.text', 
                    'connectToField': 'text', 
                    'as': 'relatedTerms', 
                    'depthField': 'depth', 
                    'maxDepth': 2
                }
            }
        ],
        "query_type": "aggregate",
        "classe_query": "Busca por termos caminhando no grafo",
        "detalhe_query": "Caminha 4 arestas do grafo"
    },
    {
        "query": [
            {
                '$match': {
                    'text': {
                        '$in': [
                            'coral'
                        ]
                    }, 
                    'terms': {
                        '$ne': []
                    }
                }
            }, {
                '$graphLookup': {
                    'from': 'termsBase', 
                    'startWith': '$terms.text', 
                    'connectFromField': 'terms.text', 
                    'connectToField': 'text', 
                    'as': 'relatedTerms', 
                    'depthField': 'depth', 
                    'maxDepth': 3
                }
            }
        ],
        "query_type": "aggregate",
        "classe_query": "Busca por termos caminhando no grafo",
        "detalhe_query": "Caminha 5 arestas do grafo"
    }
]
In [4]:
df_data = {
    "classe_query": list(),
    "detalhe_query": list(),
    "classe_detalhada": list(),
    "num_docs_resposta": list(),
    "num_relacoes": list(),
    "tempo_resposta_ms": list(),
    "iteração": list()
}

for query in queries:
    for i in range(50):
        if query["query_type"] == "find":
            query_function = collection.find
        elif query["query_type"] == "aggregate":
            query_function = collection.aggregate
        
        start = time.time()
        res = list(query_function(query["query"]))
        end = time.time()
        tempo_resposta_ms = (end - start) * 1000
        num_docs = len(res)
        num_relacoes = 0
        for doc in res:
            num_relacoes += len(doc["terms"])
            if "relatedTerms" in doc.keys():
                num_relacoes += sum([len(rt["terms"]) for rt in doc["relatedTerms"]])

        df_data["classe_query"].append(query["classe_query"])
        df_data["detalhe_query"].append(query["detalhe_query"])
        df_data["classe_detalhada"].append(query["classe_query"] + " - " + query["detalhe_query"])
        df_data["num_docs_resposta"].append(num_docs)
        df_data["num_relacoes"].append(num_relacoes)
        df_data["tempo_resposta_ms"].append(tempo_resposta_ms)
        df_data["iteração"].append(i + 1)

tempo_resposta_df = pd.DataFrame(df_data)
tempo_resposta_df.head()
Out[4]:
classe_query detalhe_query classe_detalhada num_docs_resposta num_relacoes tempo_resposta_ms iteração
0 Busca por source Permite outras fontes Busca por source - Permite outras fontes 12167 69364 243.000269 1
1 Busca por source Permite outras fontes Busca por source - Permite outras fontes 12167 69364 414.038181 2
2 Busca por source Permite outras fontes Busca por source - Permite outras fontes 12167 69364 177.009106 3
3 Busca por source Permite outras fontes Busca por source - Permite outras fontes 12167 69364 285.044670 4
4 Busca por source Permite outras fontes Busca por source - Permite outras fontes 12167 69364 255.010605 5

Verificando retornos das queries¶

Antes de analisar o tempo de resposta das queries, vejamos a quantidade de documentos retornados por elas:

In [5]:
data_viz = tempo_resposta_df.groupby(
    "classe_detalhada"
).agg(
    {"num_docs_resposta": "mean"}
).reset_index()

fig = px.bar(
    data_viz, x='classe_detalhada', y='num_docs_resposta', log_y=True,
    title="Quantidade de documentos por consulta",
    labels={
        "classe_detalhada": "Consulta",
        "num_docs_resposta": "Quantidade de documentos",
    }
)
fig.show()

Podemos ver que grande parte dos documentos retornam apenas um documento, exceto a busca por source e a que utiliza regex.

Vejamos agora a quantidade de relações retornadas por cada query.

In [6]:
data_viz = tempo_resposta_df.groupby(
    "classe_detalhada"
).agg(
    {"num_relacoes": "mean"}
).reset_index()

fig = px.bar(
    data_viz, x='classe_detalhada', y='num_relacoes', log_y=True,
    title="Quantidade de termos relacionados por consulta",
    labels={
        "classe_detalhada": "Consulta",
        "num_relacoes": "Quantidade de termos relacionados",
    }
)
fig.show()

Podemos ver que a quantidade de termos relacionados está proporcional a quantidade de documentos, exceto pelas queries que caminham no grafo, onde apenas um documento possui centenas de relacionamentos.

Análise do tempo de resposta¶

Vejamos agora o tempo de resposta das queries. Vamos iniciar vendo o tempo de resposta por classe de query.

In [7]:
fig = px.box(
    tempo_resposta_df, x="classe_query", y="tempo_resposta_ms",
    title="Tempo de resposta por classe de consulta",
    labels={
        "classe_query": "Classe de consulta",
        "tempo_resposta_ms": "Tempo de resposta (ms)",
    }
)
fig.show()

Podemos ver que a busca por source é a que mais demora, tendo em vista que recupera todos os documentos do Tesauro de Tulsa. Mesmo a query retornando na casa de 10 mil documentos, ela retornou os resultados em menos de um milissegundo.

Vejamos agora o tempo de resposta de cada query.

In [8]:
fig = px.box(
    tempo_resposta_df, x="classe_detalhada", y="tempo_resposta_ms", color="classe_query", boxmode="overlay",
    title="Tempo de resposta por consulta",
    labels={
        "classe_detalhada": "Consulta",
        "tempo_resposta_ms": "Tempo de resposta (ms)",
        "classe_query": "Classe de consulta"
    },
    height=500
)
fig.show()

Podemos ver que o dentre as queries que não buscam todos os documentos de um determinado source, as que mais demoram são a que busca tanto por termo quanto por relação, seguido da que busca apenas o termo por regex. A eficiência das queries que caminham no grafo surpreendem, pois apesar de terem documentos com até milhares de relacionamentos, tiveram um tempo de resposta inferior as demais citadas acima.

Conclusão¶

Nesta análise pudemos ver que as queries realizadas no mongo, independente de sua complexidade, retornam em um tempo bastante razoável. As queries que mais demoraram tiveram a sua demora associada a um número alto de documentos retornados.